{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Using TF-TRT With Tensorflow 2:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Tensorflow/TensorRT integration (TF-TRT) is a high level Python interface for TensorRT that works directly with Tensorflow models. In Tensorflow 2, TF-TRT allows you to convert Tensorflow SavedModels to TensorRT optimized models and run them within Python. This is a simple and flexible way to get started with TensorRT when using Tensorflow.\n",
    "\n",
    "This notebook provides a basic introduction and wrapper that makes it easy to work with basic Keras/TF2 models. We will take a pretrained Resnet-50 model from the keras.applications model zoo, convert it using TF-TRT, and run it in the TF-TRT Python runtime!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Use this when:\n",
    "- You want the API with the least dependencies\n",
    "- You are willing to give up some optimizations in exchange for more flexibility\n",
    "- You have a network which contains operations unsupported by the ONNX parser but still want to use an automatic parser\n",
    "- You do not want to write custom C++ plugins/optimizations if your network has unsupported operations\n",
    "- You are okay with being limited to the Tensorflow or TRITON runtimes in most cases"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For the TF-TRT portion of this guide, we will be using a wrapper included with the notebooks in the [TensorRT OSS examples](https://github.com/NVIDIA/TensorRT).\n",
    "\n",
    "You can clone the entire repository and work inside it, or you can grab just the wrapper by:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--2021-01-29 23:37:25--  https://raw.githubusercontent.com/NVIDIA/TensorRT/master/quickstart/IntroNotebooks/helper.py\n",
      "Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.40.133\n",
      "Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.40.133|:443... connected.\n",
      "HTTP request sent, awaiting response... 404 Not Found\n",
      "2021-01-29 23:37:25 ERROR 404: Not Found.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "!wget \"https://raw.githubusercontent.com/NVIDIA/TensorRT/master/quickstart/IntroNotebooks/helper.py\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Checking your GPU status:__\n",
    "\n",
    "Lets see what GPU hardware we are working with. Our hardware can matter a lot because different cards have different performance profiles and precisions they tend to operate best in. For example, a V100 is relatively strong as FP16 processing vs a T4, which tends to operate best in the INT8 mode."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fri Jan 29 23:37:26 2021       \n",
      "+-----------------------------------------------------------------------------+\n",
      "| NVIDIA-SMI 450.80.02    Driver Version: 450.80.02    CUDA Version: 11.1     |\n",
      "|-------------------------------+----------------------+----------------------+\n",
      "| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |\n",
      "| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |\n",
      "|                               |                      |               MIG M. |\n",
      "|===============================+======================+======================|\n",
      "|   0  Tesla V100-DGXS...  On   | 00000000:07:00.0 Off |                    0 |\n",
      "| N/A   42C    P0    37W / 300W |    125MiB / 16155MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   1  Tesla V100-DGXS...  On   | 00000000:08:00.0 Off |                    0 |\n",
      "| N/A   42C    P0    38W / 300W |      6MiB / 16158MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   2  Tesla V100-DGXS...  On   | 00000000:0E:00.0 Off |                    0 |\n",
      "| N/A   41C    P0    38W / 300W |      6MiB / 16158MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "|   3  Tesla V100-DGXS...  On   | 00000000:0F:00.0 Off |                    0 |\n",
      "| N/A   42C    P0    37W / 300W |      6MiB / 16158MiB |      0%      Default |\n",
      "|                               |                      |                  N/A |\n",
      "+-------------------------------+----------------------+----------------------+\n",
      "                                                                               \n",
      "+-----------------------------------------------------------------------------+\n",
      "| Processes:                                                                  |\n",
      "|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |\n",
      "|        ID   ID                                                   Usage      |\n",
      "|=============================================================================|\n",
      "+-----------------------------------------------------------------------------+\n"
     ]
    }
   ],
   "source": [
    "!nvidia-smi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Basic usage: Optimizing a TF2/Keras model with TensorRT in FP32:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remember to sucessfully deploy a TensorRT model, you have to answer __five important questions__:\n",
    "\n",
    "1. __What format should I save my model in?__\n",
    "2. __What batch size(s) am I running inference at?__\n",
    "3. __What precision am I running inference at?__\n",
    "4. __What TensorRT path am I using to convert my model?__\n",
    "5. __What runtime am I targeting?__\n",
    "\n",
    "We will be following this path to convert and deploy our model:\n",
    "\n",
    "![TF-TRT](./images/tf_trt.png)\n",
    "\n",
    "Lets address these five questions here!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. What format should I save my model in?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For TF-TRT, we need our models to be in [SavedModel format](https://www.tensorflow.org/guide/saved_model). We can load up, for example, a Keras model and save it appropriately as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "!mkdir -p tmp_savedmodels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tensorflow.keras.applications import ResNet50\n",
    "\n",
    "model_dir = 'tmp_savedmodels/resnet50_saved_model'\n",
    "model = ResNet50(include_top=True, weights='imagenet')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/training/tracking/tracking.py:111: Model.state_updates (from tensorflow.python.keras.engine.training) is deprecated and will be removed in a future version.\n",
      "Instructions for updating:\n",
      "This property should not be used in TensorFlow 2.0, as updates are applied automatically.\n",
      "WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/training/tracking/tracking.py:111: Layer.updates (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.\n",
      "Instructions for updating:\n",
      "This property should not be used in TensorFlow 2.0, as updates are applied automatically.\n",
      "INFO:tensorflow:Assets written to: tmp_savedmodels/resnet50_saved_model/assets\n"
     ]
    }
   ],
   "source": [
    "model.save(model_dir) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. What batch size(s) am I running inference at?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we generate a dummy batch of data to pass into the network just to get an understanding of its performance. This is normally where you would supply a numpy batch of images."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "BATCH_SIZE = 32\n",
    "\n",
    "dummy_input_batch = np.zeros((BATCH_SIZE, 224, 224, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. What precision am I running inference at?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will start with FP32 precision as a baseline! Later in this notebook, we will go through and look at how we can reduce our precision from the default."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "PRECISION = \"FP32\" # Options are \"FP32\", \"FP16\", or \"INT8\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. What TensorRT path am I using to convert my model?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will be using a simplified wrapper (ModelOptimizer) around TF-TRT to handle our conversions for this notebook. The wrapper is bare bones, meant as a springboard for further develoment - not a finished product. It can help us easily and quickly convert a TF-TRT model to a number of precisions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from helper import ModelOptimizer # using the helper from <URL>\n",
    "\n",
    "model_dir = 'tmp_savedmodels/resnet50_saved_model'\n",
    "\n",
    "opt_model = ModelOptimizer(model_dir)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:tensorflow:Linked TensorRT version: (7, 2, 1)\n",
      "INFO:tensorflow:Loaded TensorRT version: (7, 2, 2)\n",
      "INFO:tensorflow:Loaded TensorRT 7.2.2 and linked TensorFlow against TensorRT 7.2.1. This is supported because TensorRT  minor/patch upgrades are backward compatible\n",
      "INFO:tensorflow:Could not find TRTEngineOp_0_0 in TF-TRT cache. This can happen if build() is not called, which means TensorRT engines will be built and cached at runtime.\n",
      "INFO:tensorflow:Assets written to: tmp_savedmodels/resnet50_saved_model_FP32/assets\n"
     ]
    }
   ],
   "source": [
    "model_fp32 = opt_model.convert(model_dir+'_FP32', precision=PRECISION)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. What TensorRT runtime am I targeting?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "TF-TRT essentially yields a Tensorflow graph with some optimized TensorRT operations included in it. We can run this graph with .predict() like we would any other Tensorflow model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       ...,\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04]], dtype=float32)"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model_fp32.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have a finished TF-TRT optimized Tensorflow graph!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__We can now compare the TensorRT optimized model with the original:__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       ...,\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04],\n",
       "       [1.6964252e-04, 3.3007402e-04, 6.1350249e-05, ..., 1.4622317e-05,\n",
       "        1.4449877e-04, 6.6086568e-04]], dtype=float32)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Warm up - the first batch through a model generally takes longer\n",
    "model.predict(dummy_input_batch)\n",
    "model_fp32.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "53.5 ms ± 423 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "\n",
    "model.predict_on_batch(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "29.5 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "\n",
    "model_fp32.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reducing Precision:\n",
    "\n",
    "Inference typically requires less numeric precision than training. With some care, lower precision can give you faster computation and lower memory consumption without sacrificing any meaningful accuracy. TensorRT supports TF32, FP32, FP16, and INT8 precisions."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Reducing precision to FP16:__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "FP16 \"mixed precision\" inference gives up some accuracy in exchange for faster models with lower latency and lower memory footprint. In practice, the accuracy loss is generally negligible in FP16 - so FP16 is a fairly safe bet in most cases for inference. Cards that are focused on deep learning training often have strong FP16 capabilities, making FP16 a great choice for GPUs that are expected to be used for both training and inference - such as the NVIDIA V100\n",
    "\n",
    "Let's convert our model to FP16 and see how it performs:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:tensorflow:Linked TensorRT version: (7, 2, 1)\n",
      "INFO:tensorflow:Loaded TensorRT version: (7, 2, 2)\n",
      "INFO:tensorflow:Loaded TensorRT 7.2.2 and linked TensorFlow against TensorRT 7.2.1. This is supported because TensorRT  minor/patch upgrades are backward compatible\n",
      "INFO:tensorflow:Could not find TRTEngineOp_1_0 in TF-TRT cache. This can happen if build() is not called, which means TensorRT engines will be built and cached at runtime.\n",
      "INFO:tensorflow:Assets written to: tmp_savedmodels/resnet50_saved_model_FP16/assets\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "array([[1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04],\n",
       "       [1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04],\n",
       "       [1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04],\n",
       "       ...,\n",
       "       [1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04],\n",
       "       [1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04],\n",
       "       [1.7182514e-04, 3.3864001e-04, 6.3493084e-05, ..., 1.5010530e-05,\n",
       "        1.4759685e-04, 6.7664997e-04]], dtype=float32)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model_fp16 = opt_model.convert(model_dir+'_FP16', precision=\"FP16\")\n",
    "\n",
    "model_fp16.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "13.5 ms ± 20.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "\n",
    "model_fp16.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Reducing precision to INT8:__\n",
    "\n",
    "Whether you want to further reduce to INT8 precision depends on hardware - Turing cards and later INT8 is often better. Inference focused cards such as the NVIDIA T4 or systems-on-module such as Jetson AGX Xavier do well with INT8. In contrast, on a training-focused GPU like V100, INT8 often isn't any faster than FP16."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To perform INT8 inference, we need to see what the normal range of activations are in the network so we can quantize our INT8 representations based on a normal set of values for our dataset. It is important that this dataset is representative of the testing samples in order to maintain accuracy levels.\n",
    "\n",
    "Here, we just want to see how our network performs in TensorRT from a runtime standpoint - so we will just feed dummy data and dummy calibration data into TensorRT."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "dummy_calibration_batch = np.zeros((8, 224, 224, 3))\n",
    "\n",
    "opt_model.set_calibration_data(dummy_calibration_batch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, we convert our model to INT8 as before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO:tensorflow:Linked TensorRT version: (7, 2, 1)\n",
      "INFO:tensorflow:Loaded TensorRT version: (7, 2, 2)\n",
      "INFO:tensorflow:Loaded TensorRT 7.2.2 and linked TensorFlow against TensorRT 7.2.1. This is supported because TensorRT  minor/patch upgrades are backward compatible\n",
      "INFO:tensorflow:Assets written to: tmp_savedmodels/resnet50_saved_model_INT8/assets\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "array([[1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04],\n",
       "       [1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04],\n",
       "       [1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04],\n",
       "       ...,\n",
       "       [1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04],\n",
       "       [1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04],\n",
       "       [1.61497956e-04, 3.58211488e-04, 7.12977999e-05, ...,\n",
       "        1.43723055e-05, 1.47045619e-04, 7.21490127e-04]], dtype=float32)"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model_int8 = opt_model.convert(model_dir+'_INT8', precision=\"INT8\")\n",
    "\n",
    "model_int8.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "13.1 ms ± 29.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "\n",
    "model_int8.predict(dummy_input_batch)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Next Steps:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can find other Jupyter Notebooks demonstrating TF-TRT conversions and end to end workflows for many other Keras applications and models, including detection models and segmentation models, in other example TF-TRT notebooks!\n",
    "\n",
    "Here are links to those notebooks:\n",
    "\n",
    "[__Classification Examples__](./Additional%20Examples/1.%20TF-TRT%20Classification.ipynb)\n",
    "\n",
    "[__Detection Example__](./Additional%20Examples/2.%20TF-TRT%20Detection.ipynb)\n",
    "\n",
    "[__Segmentation Example__](./Additional%20Examples/3.%20TF-TRT%20Segmentation.ipynb)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
